// 优化点已应用，功能不变，字符串和输入输出保持一致
package com.zzvcom.util;

import cn.hutool.core.thread.BlockPolicy;
import lombok.extern.slf4j.Slf4j;

import javax.net.ssl.*;
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.LongAdder;
import java.util.zip.GZIPInputStream;

/**
 * 下载 服务
 * ChunkDownloader类，具备以下功能：
 * 分块下载：文件大于50MB时启用多线程分块下载，每块10MB。
 * 单线程下载：小文件使用单线程下载。
 * 断点重试：支持HTTP请求重试与分片重试。
 * 进度监听：通过DownloadListener回调报告进度、完成或错误。
 * 限速控制：支持下载速度限制。
 * HTTPS与代理支持：可配置SSL和代理。
 * 线程池管理：使用线程池控制并发下载任务
 */
@Slf4j
public class ChunkDownloader {
    private final int THREAD_COUNT = Runtime.getRuntime().availableProcessors();
    private final Map<String, DownloadState> DOWNLOAD_STATES = new ConcurrentHashMap<>();
    /**
     * 10m一块
     */
    private final long MAX_CHUNK_SIZE = 1024 * 1024 * 10;
    /**
     * 超过50m，可是分块下载。
     */
    private final long SHOULD_CHUNK_SIZE = 1024 * 1024 * 50;
    /**
     * 多线程下载一个文件的情况，线程池
     */
    private ExecutorService chunkDownloadExecute;
    /**
     * 单文件下载器线程池
     */
    private ExecutorService singleDownloadExecute;
    /**
     * 下载配置
     */
    private DownloadConfig config;
    /**
     * 下载侦听器
     */
    private DownloadListener listener;

    /**
     * 下载侦听器
     *
     * @param config
     * @param listener
     */
    public ChunkDownloader(DownloadConfig config, DownloadListener listener) {
        this.config = config;
        this.listener = listener;
        singleDownloadExecute = new ThreadPoolExecutor(THREAD_COUNT, THREAD_COUNT * 2,
                60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(16),
                new BlockPolicy());
        chunkDownloadExecute = new ThreadPoolExecutor(THREAD_COUNT, THREAD_COUNT * 4,
                60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(16),
                new BlockPolicy());
    }

    /**
     * 使用的时候，请勿忘记调用。
     */
    public void close() {
        try {
            chunkDownloadExecute.shutdown();
            chunkDownloadExecute.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            log.error("close EXECUTOR chunkDownloader error", e);
        }
        try {
            singleDownloadExecute.shutdown();
            singleDownloadExecute.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            log.error("close  singleDownloadExecutes chunkDownloader error", e);
        }
    }

    /**
     * 判断该url是否正在下载中，限制相同的url地址，只能并能并发下载。
     *
     * @param url
     * @return
     */
    public boolean isDownloading(String url) {
        return DOWNLOAD_STATES.containsKey(url);
    }

    private static class DownloadState {
        final LongAdder progress = new LongAdder();
        final AtomicBoolean success = new AtomicBoolean(true);
        final long totalSize;

        public DownloadState(long totalSize) {
            this.totalSize = totalSize;
        }
    }

    public static class FileMeta {
        final long fileSize;
        final boolean supportRange;

        public FileMeta(long size, boolean support) {
            this.fileSize = size;
            this.supportRange = support;
        }
    }

    public static class DownloadConfig {
        public int retryCount = 3;
        public Proxy proxy = null;
        public SSLSocketFactory sslSocketFactory = null;
        public long speedLimit = Long.MAX_VALUE;
        public int maxRedirects = 5;
        //16KB
        private int bufferSize = 16384;
    }

    public void downloadFile(FileItem fileItem, String outputPath) throws Exception {
        if (isDownloading(fileItem.getUrl())) {
            log.error("请勿重复下载，文件正在下载中: {}", fileItem.getUrl());
            return;
        }
        FileMeta meta = null;
        int redirectCount = 0;
        while (redirectCount++ < config.maxRedirects) {
            try {
                meta = getFileMeta(fileItem.getUrl());
                if (meta != null) break;
            } catch (IOException e) {
                if (redirectCount == config.maxRedirects) throw e;
                log.warn("获取文件元信息失败，重试中..., 剩余次数: {}", config.maxRedirects - redirectCount);
                Thread.sleep(1000);
            }
        }
        if (meta == null) {
            throw new IOException("无法获取文件元信息");
        }

        if (!meta.supportRange || meta.fileSize <= SHOULD_CHUNK_SIZE) {
            // 单线程下载，不堵塞，同时并行的个数默认为CPU核数*2
            FileMeta finalMeta = meta;
            singleDownloadExecute.execute(() -> {
                DownloadState state = new DownloadState(finalMeta.fileSize);
                DOWNLOAD_STATES.put(fileItem.getUrl(), state);
                listener.onProgress(fileItem, outputPath, 0, finalMeta.fileSize);
                try {
                    log.warn("使用单线程下载,url={}", fileItem.getUrl());
                    downloadSingleFile(fileItem, outputPath, finalMeta.fileSize, state.progress, state.success);
                    if (state.success.get()) {
                        //最终成功的
                        listener.onComplete(fileItem, outputPath);
                    } else {
                        //存在下载失败的需要删除文件
                        File file = new File(outputPath);
                        if (file.exists()&&!file.delete()) {
                            log.warn("single 清理不完整文件失败");
                        }
                    }
                } finally {
                    DOWNLOAD_STATES.remove(fileItem.getUrl());
                }
            });
        } else {
            //多线程下载堵塞，因为分块使用多线程的方式下载。
            DownloadState state = new DownloadState(meta.fileSize);
            DOWNLOAD_STATES.put(fileItem.getUrl(), state);
            listener.onProgress(fileItem, outputPath, 0, meta.fileSize);
            try {
                log.warn("使用多线程下载,url={}", fileItem.getUrl());
                downloadMultiThreaded(fileItem, outputPath, meta.fileSize, state.progress, state.success);
                if (state.success.get()) {
                    //最终成功
                    listener.onComplete(fileItem, outputPath);
                } else {
                    //存在下载失败的需要删除文件
                    File file = new File(outputPath);
                    if (file.exists()&&!file.delete()) {
                        log.warn("清理不完整文件失败");
                    }
                }
            } finally {
                DOWNLOAD_STATES.remove(fileItem.getUrl());
            }
        }
    }

    private void downloadMultiThreaded(FileItem fileItem, String outputPath, long fileSize, LongAdder progress, AtomicBoolean success) throws Exception {
        File file = new File(outputPath);
        if (file.exists() && !file.delete()) {
            log.warn("文件已存在,无法删除,outputPath={}", outputPath);
        }
        if (!file.createNewFile()) {
            log.warn("无法创建文件,outputPath={}", outputPath);
        }
        try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
            raf.setLength(fileSize);
        }
        long chunkCount = (fileSize + MAX_CHUNK_SIZE - 1) / MAX_CHUNK_SIZE;
        List<CompletableFuture<Void>> futureList = new ArrayList<>();

        for (int i = 0; i < chunkCount; i++) {
            long start = i * MAX_CHUNK_SIZE;
            long end = Math.min(start + MAX_CHUNK_SIZE - 1, fileSize - 1);
            futureList.add(CompletableFuture.runAsync(
                    new ChunkDownloadTask(fileItem, start, end, outputPath, fileSize, config, listener, progress, success), chunkDownloadExecute
            ));
        }
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]));
        allFutures.exceptionally(ex -> {
            success.set(false);
            listener.onError(fileItem, outputPath, ex);
            return null;
        });

        try {
            allFutures.join();
        } catch (CompletionException e) {
            success.set(false);
            //使用原始异常
            Throwable cause = e.getCause();
            listener.onError(fileItem, outputPath, cause != null ? cause : e);
        }
    }

    static class ChunkDownloadTask implements Runnable {
        private final FileItem fileItem;
        private final long start;
        private final long end;
        private final String outputPath;
        private final long totalSize;
        private final DownloadConfig config;
        private final DownloadListener listener;
        private final LongAdder progress;
        private final AtomicBoolean success;

        public ChunkDownloadTask(FileItem fileItem, long start, long end, String outputPath, long totalSize,
                                 DownloadConfig config, DownloadListener listener, LongAdder progress, AtomicBoolean success) {
            this.fileItem = fileItem;
            this.start = start;
            this.end = end;
            this.outputPath = outputPath;
            this.totalSize = totalSize;
            this.config = config;
            this.listener = listener;
            this.progress = progress;
            this.success = success;
        }

        @Override
        public void run() {
            int retry = config.retryCount;
            File file = new File(outputPath);

            while (retry-- > 0 && success.get()) {
                if (Thread.interrupted()) {
                    Thread.currentThread().interrupt();
                    success.set(false);
                    return;
                }
                HttpURLConnection conn = null;
                try {
                    URL downloadUrl = new URI(fileItem.getUrl()).toURL();
                    conn = (HttpURLConnection) (config.proxy == null ? downloadUrl.openConnection() : downloadUrl.openConnection(config.proxy));
                    if (conn instanceof HttpsURLConnection && config.sslSocketFactory != null) {
                        ((HttpsURLConnection) conn).setSSLSocketFactory(config.sslSocketFactory);
                    }
                    conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
                    conn.setRequestProperty("Connection", "keep-alive");
                    conn.setRequestProperty("Accept-Encoding", "gzip");
                    conn.setRequestProperty("User-Agent", "Mozilla/5.0");
                    conn.setConnectTimeout(5000);
                    conn.setReadTimeout(15000);
                    int responseCode = conn.getResponseCode();
                    if (responseCode != HttpURLConnection.HTTP_PARTIAL && responseCode != HttpURLConnection.HTTP_OK) {
                        log.warn("HTTP 错误码: {}, 跳过分片下载", responseCode);
                        success.set(false);
                        return;
                    }
                    try (
                            InputStream inputStream = "gzip".equalsIgnoreCase(conn.getHeaderField("Content-Encoding"))
                                    ? new GZIPInputStream(conn.getInputStream())
                                    : conn.getInputStream();
                            ReadableByteChannel inputChannel = Channels.newChannel(inputStream);
                            RandomAccessFile raf = new RandomAccessFile(file, "rw");
                            FileChannel outputChannel = raf.getChannel()
                    ) {
                        raf.seek(start);

                        long remaining = end - start + 1;
                        long lastProgress = 0;

                        ByteBuffer buffer = ByteBuffer.allocateDirect(config.bufferSize);
                        long position = start;
                        while (remaining > 0 && success.get()) {
                            int bytesRead = inputChannel.read(buffer);
                            if (bytesRead == -1) {
                                log.warn("输入流提前结束");
                                success.set(false);
                                break;
                            }

                            buffer.flip();
                            while (buffer.hasRemaining()) {
                                outputChannel.write(buffer, position);
                            }
                            buffer.clear();
                            position += bytesRead;
                            remaining -= bytesRead;
                            progress.add(bytesRead);
                            long total = progress.sum();
                            // 每5%更新一次进度，而不是1%
                            if (totalSize > 0 && (total - lastProgress >= totalSize / 20)) {
                                listener.onProgress(fileItem, outputPath, total, totalSize);
                                lastProgress = total;
                            }
                        }
                        if (remaining == 0) {
                            log.debug("分片下载完成：{} - {}", start, end);
                        } else {
                            log.warn("分片下载未完成，剩余字节数：{}", remaining);
                            success.set(false);
                        }
                        break;
                    } catch (IOException e) {
                        log.error("文件写入失败: {}", e.getMessage());
                        success.set(false);
                        listener.onError(fileItem, outputPath, e);
                    }
                } catch (Exception e) {
                    if (retry == 0) {
                        log.error("下载失败: {}", e.getMessage());
                        success.set(false);
                        listener.onError(fileItem, outputPath, e);
                    } else {
                        log.warn("下载失败，重试中..., 剩余次数: {}", retry);
                    }
                } finally {
                    if (conn != null) {
                        conn.disconnect();
                    }
                }
            }
        }
    }

    private FileMeta getFileMeta(String url) throws Exception {
        URL downloadUrl = new URI(url).toURL();
        HttpURLConnection conn = null;
        for (int retry = 0; retry < config.retryCount; retry++) {
            try {
                conn = (HttpURLConnection) (config.proxy == null ? downloadUrl.openConnection() : downloadUrl.openConnection(config.proxy));
                if (conn instanceof HttpsURLConnection && config.sslSocketFactory != null) {
                    ((HttpsURLConnection) conn).setSSLSocketFactory(config.sslSocketFactory);
                }
                conn.setRequestMethod("HEAD");
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(5000);
                conn.setRequestProperty("Connection", "keep-alive");
                conn.setRequestProperty("User-Agent", "Mozilla/5.0");
                conn.setRequestProperty("Referer", url);
                conn.setInstanceFollowRedirects(false);
                int code = conn.getResponseCode();
                if (code == HttpURLConnection.HTTP_MOVED_TEMP || code == HttpURLConnection.HTTP_MOVED_PERM) {
                    String location = conn.getHeaderField("Location");
                    if (location != null && retry < config.maxRedirects) {
                        url = URLDecoder.decode(location, "UTF-8");
                        downloadUrl = new URI(url).toURL();
                        continue;
                    } else {
                        throw new IOException("超过最大重定向次数");
                    }
                }
                if (code != HttpURLConnection.HTTP_OK) {
                    throw new IOException("无法获取文件元信息，HTTP 状态码: " + code);
                }
                long size = conn.getContentLengthLong();
                if (size <= 0) {
                    throw new IOException("服务器未返回有效文件大小");
                }
                return new FileMeta(size, "bytes".equals(conn.getHeaderField("Accept-Ranges")));
            } finally {
                if (conn != null) conn.disconnect();
            }
        }
        throw new IOException("无法获取文件元信息");
    }

    private void downloadSingleFile(FileItem fileItem, String outputPath,
                                    long totalSize, LongAdder progress, AtomicBoolean success) {
        for (int retry = 0; retry < config.retryCount; retry++) {
            HttpURLConnection conn = null;
            try {
                URL downloadUrl = new URI(fileItem.getUrl()).toURL();
                conn = (HttpURLConnection) (config.proxy == null ? downloadUrl.openConnection() : downloadUrl.openConnection(config.proxy));
                if (conn instanceof HttpsURLConnection && config.sslSocketFactory != null) {
                    ((HttpsURLConnection) conn).setSSLSocketFactory(config.sslSocketFactory);
                }
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(60000);
                conn.setRequestProperty("Connection", "keep-alive");
                conn.setRequestProperty("Accept-Encoding", "gzip");
                conn.setRequestProperty("User-Agent", "Mozilla/5.0");
                conn.setRequestProperty("Referer", fileItem.getUrl());
                if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
                    throw new IOException("下载失败，HTTP 状态码: " + conn.getResponseCode());
                }
                try (
                        InputStream inputStream = "gzip".equalsIgnoreCase(conn.getHeaderField("Content-Encoding"))
                                ? new GZIPInputStream(conn.getInputStream())
                                : conn.getInputStream();
                        FileOutputStream out = new FileOutputStream(outputPath)
                ) {
                    byte[] buffer = new byte[config.bufferSize];
                    int bytesRead;
                    long downloaded = 0;
                    long startTime = System.nanoTime();
                    long lastProgress = 0;

                    while ((bytesRead = inputStream.read(buffer)) != -1 && success.get()) {
                        out.write(buffer, 0, bytesRead);
                        downloaded += bytesRead;
                        progress.add(bytesRead);
                        long total = progress.sum();
                        // 每5%更新一次进度，而不是1%
                        if (total - lastProgress >= totalSize / 20) {
                            listener.onProgress(fileItem, outputPath, total, totalSize);
                            lastProgress = total;
                        }
                        throttle(startTime, downloaded, config.speedLimit);
                    }
                    listener.onProgress(fileItem, outputPath, totalSize, totalSize);
                    break;
                } catch (IOException e) {
                    //除去损坏的文件
                    success.set(false);
                    log.error("single写入下载文件失败: {}", e.getMessage());
                    listener.onError(fileItem, outputPath, e);
                }
            } catch (Exception e) {
                if (retry == config.retryCount - 1) {
                    success.set(false);
                    log.error("single 依旧下载文件失败: {}", e.getMessage());
                    listener.onError(fileItem, outputPath, e);
                } else {
                    log.warn("single 网络获取失败，重试中..., 剩余次数: {}", config.retryCount - 1 - retry);
                }
            } finally {
                if (conn != null) {
                    conn.disconnect();
                }
            }
        }
    }

    private void throttle(long startTime, long downloaded, long speedLimit) throws IOException {
        if (speedLimit == Long.MAX_VALUE || speedLimit <= 0) return;
        long elapsed = System.nanoTime() - startTime;
        long expectedTime = (downloaded * 1_000_000_000L) / speedLimit;
        if (elapsed < expectedTime) {
            long sleepNanos = expectedTime - elapsed;
            long sleepMillis = sleepNanos / 1_000_000;
            int extraNanos = (int) (sleepNanos % 1_000_000);
            try {
                Thread.sleep(sleepMillis, extraNanos);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new IOException("下载限速中断", e);
            }
        }
    }

    public interface DownloadListener {
        void onProgress(FileItem fileItem, String outputFile, long downloaded, long total);

        void onComplete(FileItem fileItem, String outputFile);

        void onError(FileItem fileItem, String outputFile, Throwable e);
    }
}
